/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.aop.framework;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.Interceptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.Advisor;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry;
import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry;
import org.springframework.aop.framework.adapter.UnknownAdviceTypeException;
import org.springframework.aop.target.SingletonTargetSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.FactoryBeanNotInitializedException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;

/**
 * {@link org.springframework.beans.factory.FactoryBean} implementation that builds an
 * AOP proxy based on beans in Spring {@link org.springframework.beans.factory.BeanFactory}.
 *
 * <p>{@link org.aopalliance.intercept.MethodInterceptor MethodInterceptors} and
 * {@link org.springframework.aop.Advisor Advisors} are identified by a list of bean
 * names in the current bean factory, specified through the "interceptorNames" property.
 * The last entry in the list can be the name of a target bean or a
 * {@link org.springframework.aop.TargetSource}; however, it is normally preferable
 * to use the "targetName"/"target"/"targetSource" properties instead.
 *
 * <p>Global interceptors and advisors can be added at the factory level. The specified
 * ones are expanded in an interceptor list where an "xxx*" entry is included in the
 * list, matching the given prefix with the bean names (e.g. "global*" would match
 * both "globalBean1" and "globalBean2", "*" all defined interceptors). The matching
 * interceptors get applied according to their returned order value, if they implement
 * the {@link org.springframework.core.Ordered} interface.
 *
 * <p>Creates a JDK proxy when proxy interfaces are given, and a CGLIB proxy for the
 * actual target class if not. Note that the latter will only work if the target class
 * does not have final methods, as a dynamic subclass will be created at runtime.
 *
 * <p>It's possible to cast a proxy obtained from this factory to {@link Advised},
 * or to obtain the ProxyFactoryBean reference and programmatically manipulate it.
 * This won't work for existing prototype references, which are independent. However,
 * it will work for prototypes subsequently obtained from the factory. Changes to
 * interception will work immediately on singletons (including existing references).
 * However, to change interfaces or target it's necessary to obtain a new instance
 * from the factory. This means that singleton instances obtained from the factory
 * do not have the same object identity. However, they do have the same interceptors
 * and target, and changing any reference will change all objects.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @see #setInterceptorNames
 * @see #setProxyInterfaces
 * @see org.aopalliance.intercept.MethodInterceptor
 * @see org.springframework.aop.Advisor
 * @see Advised
 */
@SuppressWarnings("serial")
public class ProxyFactoryBean extends ProxyCreatorSupport
        implements FactoryBean<Object>, BeanClassLoaderAware, BeanFactoryAware {

    /**
     * This suffix in a value in an interceptor list indicates to expand globals.
     */
    public static final String GLOBAL_SUFFIX = "*";


    protected final Log logger = LogFactory.getLog(getClass());

    @Nullable
    private String[] interceptorNames;

    @Nullable
    private String targetName;

    private boolean autodetectInterfaces = true;

    private boolean singleton = true;

    private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance();

    private boolean freezeProxy = false;

    @Nullable
    private transient ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader();

    private transient boolean classLoaderConfigured = false;

    @Nullable
    private transient BeanFactory beanFactory;

    /**
     * Whether the advisor chain has already been initialized
     */
    private boolean advisorChainInitialized = false;

    /**
     * If this is a singleton, the cached singleton proxy instance
     */
    @Nullable
    private Object singletonInstance;


    /**
     * Set the names of the interfaces we're proxying. If no interface
     * is given, a CGLIB for the actual class will be created.
     * <p>This is essentially equivalent to the "setInterfaces" method,
     * but mirrors TransactionProxyFactoryBean's "setProxyInterfaces".
     *
     * @see #setInterfaces
     * @see AbstractSingletonProxyFactoryBean#setProxyInterfaces
     */
    public void setProxyInterfaces(Class<?>[] proxyInterfaces) throws ClassNotFoundException {
        setInterfaces(proxyInterfaces);
    }

    /**
     * Set the list of Advice/Advisor bean names. This must always be set
     * to use this factory bean in a bean factory.
     * <p>The referenced beans should be of type Interceptor, Advisor or Advice
     * The last entry in the list can be the name of any bean in the factory.
     * If it's neither an Advice nor an Advisor, a new SingletonTargetSource
     * is added to wrap it. Such a target bean cannot be used if the "target"
     * or "targetSource" or "targetName" property is set, in which case the
     * "interceptorNames" array must contain only Advice/Advisor bean names.
     * <p><b>NOTE: Specifying a target bean as final name in the "interceptorNames"
     * list is deprecated and will be removed in a future Spring version.</b>
     * Use the {@link #setTargetName "targetName"} property instead.
     *
     * @see org.aopalliance.intercept.MethodInterceptor
     * @see org.springframework.aop.Advisor
     * @see org.aopalliance.aop.Advice
     * @see org.springframework.aop.target.SingletonTargetSource
     */
    public void setInterceptorNames(String... interceptorNames) {
        this.interceptorNames = interceptorNames;
    }

    /**
     * Set the name of the target bean. This is an alternative to specifying
     * the target name at the end of the "interceptorNames" array.
     * <p>You can also specify a target object or a TargetSource object
     * directly, via the "target"/"targetSource" property, respectively.
     *
     * @see #setInterceptorNames(String[])
     * @see #setTarget(Object)
     * @see #setTargetSource(org.springframework.aop.TargetSource)
     */
    public void setTargetName(String targetName) {
        this.targetName = targetName;
    }

    /**
     * Set whether to autodetect proxy interfaces if none specified.
     * <p>Default is "true". Turn this flag off to create a CGLIB
     * proxy for the full target class if no interfaces specified.
     *
     * @see #setProxyTargetClass
     */
    public void setAutodetectInterfaces(boolean autodetectInterfaces) {
        this.autodetectInterfaces = autodetectInterfaces;
    }

    /**
     * Set the value of the singleton property. Governs whether this factory
     * should always return the same proxy instance (which implies the same target)
     * or whether it should return a new prototype instance, which implies that
     * the target and interceptors may be new instances also, if they are obtained
     * from prototype bean definitions. This allows for fine control of
     * independence/uniqueness in the object graph.
     */
    public void setSingleton(boolean singleton) {
        this.singleton = singleton;
    }

    /**
     * Specify the AdvisorAdapterRegistry to use.
     * Default is the global AdvisorAdapterRegistry.
     *
     * @see org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry
     */
    public void setAdvisorAdapterRegistry(AdvisorAdapterRegistry advisorAdapterRegistry) {
        this.advisorAdapterRegistry = advisorAdapterRegistry;
    }

    @Override
    public void setFrozen(boolean frozen) {
        this.freezeProxy = frozen;
    }

    /**
     * Set the ClassLoader to generate the proxy class in.
     * <p>Default is the bean ClassLoader, i.e. the ClassLoader used by the
     * containing BeanFactory for loading all bean classes. This can be
     * overridden here for specific proxies.
     */
    public void setProxyClassLoader(@Nullable ClassLoader classLoader) {
        this.proxyClassLoader = classLoader;
        this.classLoaderConfigured = (classLoader != null);
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        if (!this.classLoaderConfigured) {
            this.proxyClassLoader = classLoader;
        }
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        checkInterceptorNames();
    }


    /**
     * Return a proxy. Invoked when clients obtain beans from this factory bean.
     * Create an instance of the AOP proxy to be returned by this factory.
     * The instance will be cached for a singleton, and create on each call to
     * {@code getObject()} for a proxy.
     *
     * @return a fresh AOP proxy reflecting the current state of this factory
     */
    @Override
    @Nullable
    public Object getObject() throws BeansException {
        initializeAdvisorChain();
        if (isSingleton()) {
            return getSingletonInstance();
        } else {
            if (this.targetName == null) {
                logger.warn("Using non-singleton proxies with singleton targets is often undesirable. " +
                        "Enable prototype proxies by setting the 'targetName' property.");
            }
            return newPrototypeInstance();
        }
    }

    /**
     * Return the type of the proxy. Will check the singleton instance if
     * already created, else fall back to the proxy interface (in case of just
     * a single one), the target bean type, or the TargetSource's target class.
     *
     * @see org.springframework.aop.TargetSource#getTargetClass
     */
    @Override
    public Class<?> getObjectType() {
        synchronized (this) {
            if (this.singletonInstance != null) {
                return this.singletonInstance.getClass();
            }
        }
        Class<?>[] ifcs = getProxiedInterfaces();
        if (ifcs.length == 1) {
            return ifcs[0];
        } else if (ifcs.length > 1) {
            return createCompositeInterface(ifcs);
        } else if (this.targetName != null && this.beanFactory != null) {
            return this.beanFactory.getType(this.targetName);
        } else {
            return getTargetClass();
        }
    }

    @Override
    public boolean isSingleton() {
        return this.singleton;
    }


    /**
     * Create a composite interface Class for the given interfaces,
     * implementing the given interfaces in one single Class.
     * <p>The default implementation builds a JDK proxy class for the
     * given interfaces.
     *
     * @param interfaces the interfaces to merge
     * @return the merged interface as Class
     * @see java.lang.reflect.Proxy#getProxyClass
     */
    protected Class<?> createCompositeInterface(Class<?>[] interfaces) {
        return ClassUtils.createCompositeInterface(interfaces, this.proxyClassLoader);
    }

    /**
     * Return the singleton instance of this class's proxy object,
     * lazily creating it if it hasn't been created already.
     *
     * @return the shared singleton proxy
     */
    private synchronized Object getSingletonInstance() {
        if (this.singletonInstance == null) {
            this.targetSource = freshTargetSource();
            if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) {
                // Rely on AOP infrastructure to tell us what interfaces to proxy.
                Class<?> targetClass = getTargetClass();
                if (targetClass == null) {
                    throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy");
                }
                setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
            }
            // Initialize the shared singleton instance.
            super.setFrozen(this.freezeProxy);
            this.singletonInstance = getProxy(createAopProxy());
        }
        return this.singletonInstance;
    }

    /**
     * Create a new prototype instance of this class's created proxy object,
     * backed by an independent AdvisedSupport configuration.
     *
     * @return a totally independent proxy, whose advice we may manipulate in isolation
     */
    private synchronized Object newPrototypeInstance() {
        // In the case of a prototype, we need to give the proxy
        // an independent instance of the configuration.
        // In this case, no proxy will have an instance of this object's configuration,
        // but will have an independent copy.
        if (logger.isTraceEnabled()) {
            logger.trace("Creating copy of prototype ProxyFactoryBean config: " + this);
        }

        ProxyCreatorSupport copy = new ProxyCreatorSupport(getAopProxyFactory());
        // The copy needs a fresh advisor chain, and a fresh TargetSource.
        TargetSource targetSource = freshTargetSource();
        copy.copyConfigurationFrom(this, targetSource, freshAdvisorChain());
        if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) {
            // Rely on AOP infrastructure to tell us what interfaces to proxy.
            Class<?> targetClass = targetSource.getTargetClass();
            if (targetClass != null) {
                copy.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
            }
        }
        copy.setFrozen(this.freezeProxy);

        if (logger.isTraceEnabled()) {
            logger.trace("Using ProxyCreatorSupport copy: " + copy);
        }
        return getProxy(copy.createAopProxy());
    }

    /**
     * Return the proxy object to expose.
     * <p>The default implementation uses a {@code getProxy} call with
     * the factory's bean class loader. Can be overridden to specify a
     * custom class loader.
     *
     * @param aopProxy the prepared AopProxy instance to get the proxy from
     * @return the proxy object to expose
     * @see AopProxy#getProxy(ClassLoader)
     */
    protected Object getProxy(AopProxy aopProxy) {
        return aopProxy.getProxy(this.proxyClassLoader);
    }

    /**
     * Check the interceptorNames list whether it contains a target name as final element.
     * If found, remove the final name from the list and set it as targetName.
     */
    private void checkInterceptorNames() {
        if (!ObjectUtils.isEmpty(this.interceptorNames)) {
            String finalName = this.interceptorNames[this.interceptorNames.length - 1];
            if (this.targetName == null && this.targetSource == EMPTY_TARGET_SOURCE) {
                // The last name in the chain may be an Advisor/Advice or a target/TargetSource.
                // Unfortunately we don't know; we must look at type of the bean.
                if (!finalName.endsWith(GLOBAL_SUFFIX) && !isNamedBeanAnAdvisorOrAdvice(finalName)) {
                    // The target isn't an interceptor.
                    this.targetName = finalName;
                    if (logger.isDebugEnabled()) {
                        logger.debug("Bean with name '" + finalName + "' concluding interceptor chain " +
                                "is not an advisor class: treating it as a target or TargetSource");
                    }
                    String[] newNames = new String[this.interceptorNames.length - 1];
                    System.arraycopy(this.interceptorNames, 0, newNames, 0, newNames.length);
                    this.interceptorNames = newNames;
                }
            }
        }
    }

    /**
     * Look at bean factory metadata to work out whether this bean name,
     * which concludes the interceptorNames list, is an Advisor or Advice,
     * or may be a target.
     *
     * @param beanName bean name to check
     * @return {@code true} if it's an Advisor or Advice
     */
    private boolean isNamedBeanAnAdvisorOrAdvice(String beanName) {
        Assert.state(this.beanFactory != null, "No BeanFactory set");
        Class<?> namedBeanClass = this.beanFactory.getType(beanName);
        if (namedBeanClass != null) {
            return (Advisor.class.isAssignableFrom(namedBeanClass) || Advice.class.isAssignableFrom(namedBeanClass));
        }
        // Treat it as an target bean if we can't tell.
        if (logger.isDebugEnabled()) {
            logger.debug("Could not determine type of bean with name '" + beanName +
                    "' - assuming it is neither an Advisor nor an Advice");
        }
        return false;
    }

    /**
     * Create the advisor (interceptor) chain. Advisors that are sourced
     * from a BeanFactory will be refreshed each time a new prototype instance
     * is added. Interceptors added programmatically through the factory API
     * are unaffected by such changes.
     */
    private synchronized void initializeAdvisorChain() throws AopConfigException, BeansException {
        if (this.advisorChainInitialized) {
            return;
        }

        if (!ObjectUtils.isEmpty(this.interceptorNames)) {
            if (this.beanFactory == null) {
                throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " +
                        "- cannot resolve interceptor names " + Arrays.asList(this.interceptorNames));
            }

            // Globals can't be last unless we specified a targetSource using the property...
            if (this.interceptorNames[this.interceptorNames.length - 1].endsWith(GLOBAL_SUFFIX) &&
                    this.targetName == null && this.targetSource == EMPTY_TARGET_SOURCE) {
                throw new AopConfigException("Target required after globals");
            }

            // Materialize interceptor chain from bean names.
            for (String name : this.interceptorNames) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Configuring advisor or advice '" + name + "'");
                }

                if (name.endsWith(GLOBAL_SUFFIX)) {
                    if (!(this.beanFactory instanceof ListableBeanFactory)) {
                        throw new AopConfigException(
                                "Can only use global advisors or interceptors with a ListableBeanFactory");
                    }
                    addGlobalAdvisor((ListableBeanFactory) this.beanFactory,
                            name.substring(0, name.length() - GLOBAL_SUFFIX.length()));
                } else {
                    // If we get here, we need to add a named interceptor.
                    // We must check if it's a singleton or prototype.
                    Object advice;
                    if (this.singleton || this.beanFactory.isSingleton(name)) {
                        // Add the real Advisor/Advice to the chain.
                        advice = this.beanFactory.getBean(name);
                    } else {
                        // It's a prototype Advice or Advisor: replace with a prototype.
                        // Avoid unnecessary creation of prototype bean just for advisor chain initialization.
                        advice = new PrototypePlaceholderAdvisor(name);
                    }
                    addAdvisorOnChainCreation(advice, name);
                }
            }
        }

        this.advisorChainInitialized = true;
    }


    /**
     * Return an independent advisor chain.
     * We need to do this every time a new prototype instance is returned,
     * to return distinct instances of prototype Advisors and Advices.
     */
    private List<Advisor> freshAdvisorChain() {
        Advisor[] advisors = getAdvisors();
        List<Advisor> freshAdvisors = new ArrayList<>(advisors.length);
        for (Advisor advisor : advisors) {
            if (advisor instanceof PrototypePlaceholderAdvisor) {
                PrototypePlaceholderAdvisor pa = (PrototypePlaceholderAdvisor) advisor;
                if (logger.isDebugEnabled()) {
                    logger.debug("Refreshing bean named '" + pa.getBeanName() + "'");
                }
                // Replace the placeholder with a fresh prototype instance resulting
                // from a getBean() lookup
                if (this.beanFactory == null) {
                    throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " +
                            "- cannot resolve prototype advisor '" + pa.getBeanName() + "'");
                }
                Object bean = this.beanFactory.getBean(pa.getBeanName());
                Advisor refreshedAdvisor = namedBeanToAdvisor(bean);
                freshAdvisors.add(refreshedAdvisor);
            } else {
                // Add the shared instance.
                freshAdvisors.add(advisor);
            }
        }
        return freshAdvisors;
    }

    /**
     * Add all global interceptors and pointcuts.
     */
    private void addGlobalAdvisor(ListableBeanFactory beanFactory, String prefix) {
        String[] globalAdvisorNames =
                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Advisor.class);
        String[] globalInterceptorNames =
                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Interceptor.class);
        List<Object> beans = new ArrayList<>(globalAdvisorNames.length + globalInterceptorNames.length);
        Map<Object, String> names = new HashMap<>(beans.size());
        for (String name : globalAdvisorNames) {
            Object bean = beanFactory.getBean(name);
            beans.add(bean);
            names.put(bean, name);
        }
        for (String name : globalInterceptorNames) {
            Object bean = beanFactory.getBean(name);
            beans.add(bean);
            names.put(bean, name);
        }
        AnnotationAwareOrderComparator.sort(beans);
        for (Object bean : beans) {
            String name = names.get(bean);
            if (name.startsWith(prefix)) {
                addAdvisorOnChainCreation(bean, name);
            }
        }
    }

    /**
     * Invoked when advice chain is created.
     * <p>Add the given advice, advisor or object to the interceptor list.
     * Because of these three possibilities, we can't type the signature
     * more strongly.
     *
     * @param next advice, advisor or target object
     * @param name bean name from which we obtained this object in our owning
     *             bean factory
     */
    private void addAdvisorOnChainCreation(Object next, String name) {
        // We need to convert to an Advisor if necessary so that our source reference
        // matches what we find from superclass interceptors.
        Advisor advisor = namedBeanToAdvisor(next);
        if (logger.isTraceEnabled()) {
            logger.trace("Adding advisor with name '" + name + "'");
        }
        addAdvisor(advisor);
    }

    /**
     * Return a TargetSource to use when creating a proxy. If the target was not
     * specified at the end of the interceptorNames list, the TargetSource will be
     * this class's TargetSource member. Otherwise, we get the target bean and wrap
     * it in a TargetSource if necessary.
     */
    private TargetSource freshTargetSource() {
        if (this.targetName == null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Not refreshing target: Bean name not specified in 'interceptorNames'.");
            }
            return this.targetSource;
        } else {
            if (this.beanFactory == null) {
                throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " +
                        "- cannot resolve target with name '" + this.targetName + "'");
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Refreshing target with name '" + this.targetName + "'");
            }
            Object target = this.beanFactory.getBean(this.targetName);
            return (target instanceof TargetSource ? (TargetSource) target : new SingletonTargetSource(target));
        }
    }

    /**
     * Convert the following object sourced from calling getBean() on a name in the
     * interceptorNames array to an Advisor or TargetSource.
     */
    private Advisor namedBeanToAdvisor(Object next) {
        try {
            return this.advisorAdapterRegistry.wrap(next);
        } catch (UnknownAdviceTypeException ex) {
            // We expected this to be an Advisor or Advice,
            // but it wasn't. This is a configuration error.
            throw new AopConfigException("Unknown advisor type " + next.getClass() +
                    "; Can only include Advisor or Advice type beans in interceptorNames chain except for last entry," +
                    "which may also be target or TargetSource", ex);
        }
    }

    /**
     * Blow away and recache singleton on an advice change.
     */
    @Override
    protected void adviceChanged() {
        super.adviceChanged();
        if (this.singleton) {
            logger.debug("Advice has changed; recaching singleton instance");
            synchronized (this) {
                this.singletonInstance = null;
            }
        }
    }


    //---------------------------------------------------------------------
    // Serialization support
    //---------------------------------------------------------------------

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // Rely on default serialization; just initialize state after deserialization.
        ois.defaultReadObject();

        // Initialize transient fields.
        this.proxyClassLoader = ClassUtils.getDefaultClassLoader();
    }


    /**
     * Used in the interceptor chain where we need to replace a bean with a prototype
     * on creating a proxy.
     */
    private static class PrototypePlaceholderAdvisor implements Advisor, Serializable {

        private final String beanName;

        private final String message;

        public PrototypePlaceholderAdvisor(String beanName) {
            this.beanName = beanName;
            this.message = "Placeholder for prototype Advisor/Advice with bean name '" + beanName + "'";
        }

        public String getBeanName() {
            return beanName;
        }

        @Override
        public Advice getAdvice() {
            throw new UnsupportedOperationException("Cannot invoke methods: " + this.message);
        }

        @Override
        public boolean isPerInstance() {
            throw new UnsupportedOperationException("Cannot invoke methods: " + this.message);
        }

        @Override
        public String toString() {
            return this.message;
        }
    }

}
